﻿using System;
using System.Collections.Generic;
using System.Reflection;
using Qadir;
using Qadir.Collections.Generic;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Reflection.Emit;
using System.Runtime.InteropServices;

namespace Qadir.Runtime.InteropServices
{
    /// <summary>
    /// A wrapper around P/Invoke allowing for more efficient and organized access to unmanaged code.
    /// </summary>
    public class PInvoke
    {
        /// <summary>
        /// Instantiates the PInvoke wrapper.
        /// </summary>
        /// <param name="dlls">The locations or names of dynamically-linked libraries being accessed through this instance.</param>
        public PInvoke(IEnumerable<string> dlls = null)
        {
            DLLs = (dlls != null) ? new Manager<string>(dlls) : new Manager<string>();
        }

        /// <summary>
        /// The locations or names of dynamically-linked libraries being accessed through this instance.
        /// </summary>
        public Manager<string> DLLs { get; set; }

        /// <summary>
        /// Dynamically invokes a function from an unmanaged DLL. In the vast majority of cases, using plain P/Invoke is faster and better.
        /// </summary>
        /// <typeparam name="T">The return type of the desired method.</typeparam>
        /// <param name="dll">The name of the DLL containing the function.</param>
        /// <param name="name">The name of the method to invoke.</param>
        /// <param name="arguments">Any and all arguments required by the method.</param>
        /// <returns>Returns the result of the method, or null.</returns>
        public static T RunFunction<T>(string dll, string name, params object[] arguments)
        {
            Manager<object> mngrObjs = new Manager<object>(arguments);
            Type[] paramTypes = mngrObjs.ApplyActionToAllAndReceiveResults<Type>((arg) => { return arg.GetType(); }).ToArray();
            AssemblyName asmName = new AssemblyName("QadirDynamicPInvoke");
            AssemblyBuilder dynamicAsm = AppDomain.CurrentDomain.DefineDynamicAssembly(
                asmName,
                AssemblyBuilderAccess.RunAndSave
            );
            ModuleBuilder dynamicMod =
                dynamicAsm.DefineDynamicModule(asmName.Name, asmName.Name + ".dll");
            TypeBuilder tb = dynamicMod.DefineType(
                "QadirDynamicPInvoke",
                TypeAttributes.Public | TypeAttributes.UnicodeClass
            );
            MethodBuilder mb = tb.DefinePInvokeMethod(
                name,
                dll,
                MethodAttributes.Public | MethodAttributes.Static | MethodAttributes.PinvokeImpl,
                CallingConventions.Standard,
                typeof(T),
                paramTypes,
                CallingConvention.Winapi,
                CharSet.Ansi);
            mb.SetImplementationFlags(
                mb.GetMethodImplementationFlags() | MethodImplAttributes.PreserveSig);
            Type t = tb.CreateType();
            return (T)t.GetMethod(name).Invoke(null, arguments);
        }

        /// <summary>
        /// Queries all DLL's in the pool for a specified function, and returns the first match.
        /// </summary>
        /// <typeparam name="T">The return type of the desired method.</typeparam>
        /// <param name="name">The name of the method to invoke.</param>
        /// <param name="arguments">Any and all arguments required by the method.</param>
        /// <returns>Returns the result of the method</returns>
        public T GetFunctionFromPool<T>(string name, params object[] arguments)
        {
            foreach (string dll in DLLs)
            {
                try
                {
                    return PInvoke.RunFunction<T>(dll, name, arguments);
                }
                catch { }
            }
            return default(T);
        }
    }

    /// <summary>
    /// An exception containing details as to why a CodeDom compilation failed.
    /// </summary>
    [Serializable]
    public class CodeCompileException : Exception
    {
        /// <summary>
        /// The errors that occurred upon compilation of the code.
        /// </summary>
        public CompilerErrorCollection Errors { get; set; }

        public CodeCompileException(string message, CompilerErrorCollection errors)
            :base(message)
        {
            Errors = errors;
        }
    }
}
